package ua.pp.pycb6a.yandexdisk;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import com.github.sardine.impl.SardineImpl;
import com.github.sardine.impl.methods.HttpMkCol;
import com.github.sardine.impl.methods.HttpPropFind;
import com.github.sardine.impl.methods.HttpPropPatch;

public class YandexDiskClient implements YandexDisk {

    private static final String DIRECTORY = "Directory ";
    private static final String FILE = "File ";
    private static Logger logger = Logger.getLogger(YandexDiskClient.class
            .getName());
    private static final String HOST = "https://webdav.yandex.ru";

    private String location;
    private String localYandexDisk;
    private String login;
    private String userToken;
    private DefaultHttpClient client;
    private HttpResponse lastResponse;
    private byte[] lastEntity;
    private String lastStatus;

    private HttpRequestInterceptor requestInterceptor = new HttpRequestInterceptor() {
        @Override
        public void process(HttpRequest request, HttpContext context)
                throws HttpException, IOException {
            request.addHeader("User-Agent", "yandex-disk-client/0.0.1");
            request.addHeader("Accept-Encoding", "gzip");
            request.addHeader("Accept", "*/*");
            request.addHeader(new BasicHeader("Authorization", "Basic "
                    + userToken));

        }
    };

    private HttpResponseInterceptor responseInterceptor = new HttpResponseInterceptor() {
        @Override
        public void process(HttpResponse response, HttpContext context)
                throws HttpException, IOException {
            lastResponse = response;
        }
    };

    @Override
    public String getLocalYandexDisk() {
        return localYandexDisk;
    }

    @Override
    public void setLocalYandexDisk(String localYandexDisk) {
        this.localYandexDisk = localYandexDisk;
    }

    @Override
    public String getLogin() {
        return login;
    }

    @Override
    public void authenticate(String login, String password) {

        userToken = Base64.encodeBase64String((login + ":" + password)
                .getBytes());
        client = new DefaultHttpClient(new PoolingClientConnectionManager());

        client.addRequestInterceptor(requestInterceptor);
        client.addResponseInterceptor(responseInterceptor);
        new SardineImpl(client);

        this.login = login;
        setRootLocation();

    }

    private byte[] getLastEntity() {

        return lastEntity;
    }

    private void readLastEntity() {

        try {
            lastEntity = IOUtils.toByteArray(lastResponse.getEntity()
                    .getContent());
        } catch (IllegalStateException | IOException e) {

            logger.warning(e.getMessage());
        }

    }

    private String getLastEntityToString() {

        String stringEntity = new String(getLastEntity());
        logger.info(stringEntity);

        return stringEntity;
    }

    private File getLastEntityToFile(File file) {

        OutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            IOUtils.write(getLastEntity(), outputStream);

        } catch (IOException e) {

            logger.warning(e.getMessage());
        }

        logger.info(file.getAbsolutePath());

        return file;
    }

    private String getLastStatus() {
        return lastStatus;
    }

    private void readLastStatus() {

        lastStatus = lastResponse.getStatusLine().toString();
        logger.info(lastStatus);
    }

    private void get(String uri) {

        HttpGet get = new HttpGet(uri);

        try {
            client.execute(get);

            readLastStatus();
            readLastEntity();

        } catch (IOException e) {

            logger.warning(e.getMessage());
        } finally {

            get.releaseConnection();
        }

    }

    private void put(String uri, HttpEntity entity) {

        HttpPut put = new HttpPut(uri);
        put.setEntity(entity);

        try {
            client.execute(put);

            readLastStatus();
            readLastEntity();

        } catch (IOException e) {

            logger.warning(e.getMessage());
        } finally {

            put.releaseConnection();
        }
    }

    private void propFind(String uri, String depth, String properties) {

        HttpPropFind propFind = new HttpPropFind(uri);
        HttpEntity entity = null;
        propFind.setDepth(depth);

        try {
            if (properties.length() > 0) {
                entity = new StringEntity(properties);
            }
            propFind.setEntity(entity);
            client.execute(propFind);

            readLastStatus();
            readLastEntity();

        } catch (IOException e) {

            logger.warning(e.getMessage());
        } finally {

            propFind.releaseConnection();
        }
    }

    private void propPatch(String uri, String properties) {
        HttpPropPatch propPatch = new HttpPropPatch(uri);

        try {
            propPatch.setEntity(new StringEntity(properties));
            client.execute(propPatch);

            readLastStatus();
            readLastEntity();
        } catch (IOException e) {

            logger.warning(e.getMessage());
        } finally {

            propPatch.releaseConnection();
        }
    }

    private void mkCol(String uri) {

        HttpMkCol mkcol = new HttpMkCol(uri);
        try {
            client.execute(mkcol);

            readLastStatus();
            readLastEntity();

        } catch (IOException e) {

            logger.warning(e.getMessage());
        } finally {

            mkcol.releaseConnection();
        }
    }

    private void delete(String uri) {

        HttpDelete delete = new HttpDelete(uri);

        try {
            client.execute(delete);

            readLastStatus();
            readLastEntity();

        } catch (IOException e) {

            logger.warning(e.getMessage());
        } finally {
            delete.releaseConnection();
        }

    }

    @Override
    public String getLocation() {

        return location;
    }

    @Override
    public void setLocation(String... directories) {

        StringBuilder stringBuilder = new StringBuilder().append("/");
        for (String directory : directories) {
            stringBuilder.append(directory).append("/");
        }
        location = stringBuilder.toString();
        logger.info("Navigate to " + getLocation());
    }

    @Override
    public void setRootLocation() {
        setLocation();
    }

    @Override
    public void getFreeAndUsedSpace() {
        try {
            propFind(
                    HOST + getLocation(),
                    "0",
                    "<D:propfind xmlns:D=\"DAV:\"><D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop></D:propfind>");

            getLastStatus();
            getLastEntityToString();
        } catch (Exception e) {
            logger.warning(e.getMessage());
        }
    }

    private void getDefaultProperties(String uri, String depth) {

        logger.info("Location: " + getLocation());
        try {
            propFind(uri, depth, "");

            getLastStatus();
            getLastEntityToString();
        } catch (Exception e) {
            logger.warning(e.getMessage());
        }
    }

    @Override
    public void getCurrentDirectoryContent() {

        getDefaultProperties(HOST + getLocation(), "1");
    }

    @Override
    public void getDefaultCurrentDirectoryProperties() {

        getDefaultProperties(HOST + getLocation(), "0");
    }

    @Override
    public void getDefaultFileProperties(String filename) {

        getDefaultProperties(HOST + getLocation() + filename, "0");
    }

    @Override
    public void uploadFile(String filename) {

        put(HOST + getLocation() + filename, new FileEntity(new File(
                getLocalYandexDisk() + getLocation() + filename)));

        getLastStatus();
        getLastEntityToString();
        logger.info(FILE + getLocalYandexDisk() + getLocation() + filename
                + " is uploaded");
    }

    @Override
    public void downloadFile(String filename) {

        get(HOST + getLocation() + filename);

        getLastStatus();
        getLastEntityToFile(new File(getLocalYandexDisk() + getLocation()
                + filename));
        logger.info(FILE + getLocalYandexDisk() + getLocation() + filename
                + " is downloaded");
    }

    @Override
    public void createDirectory(String directory) {

        mkCol(HOST + getLocation() + directory);
        createLocalDirectory(getLocalYandexDisk() + getLocation() + directory);

        getLastStatus();
        getLastEntityToString();
        logger.info(DIRECTORY + getLocation() + directory + " is created");
    }

    private void createLocalDirectory(String path) {

        File file = new File(path);
        if (!file.exists()) {
            if (!file.mkdir()) {
                logger.warning("local directory " + path + " is not created");
            }
        } else {
            logger.warning("local directory " + path + " already exists");
        }
    }

    @Override
    public void deleteResource(String resource) {

        delete(HOST + getLocation() + resource);
        deleteLocalResource(getLocalYandexDisk() + getLocation() + resource);

        logger.info("Resourse " + resource + " is removed");
    }

    private void deleteLocalResource(String resource) {

        File file = new File(resource);

        if (file.isDirectory()) {
            try {
                FileUtils.deleteDirectory(file);
                if (!file.exists()) {
                    logger.info("Local directory " + resource + " is removed");
                }
            } catch (IOException e) {
                logger.warning(e.getMessage());
            }
        }

        if (file.isFile()) {
            file.delete();
            if (!file.exists()) {
                logger.info("Local file " + resource + " is removed");
            }
        }
    }

    @Override
    public void publishCurrentDirectory() {

        propPatch(
                HOST + getLocation(),
                "<propertyupdate xmlns=\"DAV:\"><set><prop><public_url xmlns=\"urn:yandex:disk:meta\">true</public_url></prop></set></propertyupdate>");
        getLastStatus();
        getLastEntityToString();
        logger.info(DIRECTORY + getLocation() + " is published");
    }

    @Override
    public void unpublishCurrentDirectory() {

        propPatch(
                HOST + getLocation(),
                "<propertyupdate xmlns=\"DAV:\"><remove><prop><public_url xmlns=\"urn:yandex:disk:meta\" /></prop></remove></propertyupdate>");
        getLastStatus();
        getLastEntityToString();
        logger.info(DIRECTORY + getLocation() + " is unpublished");
    }

    @Override
    public void publishFile(String filename) {

        propPatch(
                HOST + getLocation() + filename,
                "<propertyupdate xmlns=\"DAV:\"><set><prop><public_url xmlns=\"urn:yandex:disk:meta\">true</public_url></prop></set></propertyupdate>");
        getLastStatus();
        getLastEntityToString();
        logger.info(FILE + getLocation() + filename + " is published");
    }

    @Override
    public void unpublishFile(String filename) {

        propPatch(
                HOST + getLocation() + filename,
                "<propertyupdate xmlns=\"DAV:\"><remove><prop><public_url xmlns=\"urn:yandex:disk:meta\" /></prop></remove></propertyupdate>");
        getLastStatus();
        getLastEntityToString();
        logger.info(FILE + getLocation() + filename + " is unpublished");

    }

    private String queryXml(String expression) {

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder;
        Document doc = null;
        Node result = null;

        try {
            builder = factory.newDocumentBuilder();
            doc = builder.parse(new ByteArrayInputStream(getLastEntity()));
            XPath xPath = XPathFactory.newInstance().newXPath();

            result = (Node) xPath.compile(expression).evaluate(doc,
                    XPathConstants.NODE);

        } catch (ParserConfigurationException | XPathExpressionException
                | SAXException | IOException e) {

            logger.warning(e.getMessage());
        }

        logger.info(result.getTextContent());
        return result.getTextContent();
    }

    @Override
    public boolean isPublicCurrentDirectory() {
        propFind(
                HOST + getLocation(),
                "0",
                "<propfind xmlns=\"DAV:\"><prop><public_url xmlns=\"urn:yandex:disk:meta\"/></prop></propfind>");

        getLastStatus();
        getLastEntityToString();

        if (queryXml("//multistatus//status").contains("200")) {
            logger.info(DIRECTORY + getLocation() + " is public");

            return true;

        } else {
            logger.info(DIRECTORY + getLocation() + " is not public");
            return false;
        }

    }

    @Override
    public boolean isPublicFile(String filename) {
        propFind(
                HOST + getLocation() + filename,
                "0",
                "<propfind xmlns=\"DAV:\"><prop><public_url xmlns=\"urn:yandex:disk:meta\"/></prop></propfind>");

        getLastStatus();
        getLastEntityToString();

        if (queryXml("//multistatus//status").contains("200")) {
            logger.info(FILE + getLocation() + filename + " is public");

            return true;

        } else {
            logger.info(FILE + getLocation() + filename + " is not public");
            return false;
        }
    }

    @Override
    public String getCurrentDirectoryPublicLink() {

        if (isPublicCurrentDirectory()) {

            return queryXml("//prop/public_url");

        } else {
            return null;
        }
    }

    @Override
    public String getFilePublicLink(String filename) {

        if (isPublicFile(filename)) {

            return queryXml("//prop/public_url");

        } else {
            return null;
        }
    }
}
